﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using Pahan.McuFusion.Common;

namespace Pahan.McuFusion.Files
{
    public class IntelHexFileReader : IFileReader
    {
        public IntelHexFileReader(Stream stream)
        {
            Assert.NotNull(stream, "stream");
            if (!stream.CanRead)
            {
                throw new ArgumentException("The stream cannot be read.", "stream");
            }
            if (!stream.CanSeek)
            {
                throw new ArgumentException("The stream cannot be seeked.", "stream");
            }

            reader = new StreamReader(stream, Encoding.ASCII);
        }

        #region Implementation of IDisposable

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            reader.Dispose();
        }

        #endregion

        #region Implementation of IFileReader

        public byte DefaultValue
        {
            get;
            set;
        }

        public IList<byte> ReadAll()
        {
            List<byte> data = new List<byte>((int)(reader.BaseStream.Length / 3));
            String line;
            while ((line = reader.ReadLine()) != null)
            {
                if (!ProcessLine(line, data))
                    break;
            }
            return data;
        }

        #endregion

        #region Private Members

        private bool ProcessLine(String line, List<byte> data)
        {
            if (line[0] != ':' || line.Length < 11)
            {
                throw new FileFormatException();
            }
            try
            {
                byte length = byte.Parse(line.Substring(1, 2), NumberStyles.HexNumber);
                ushort addr = ushort.Parse(line.Substring(3, 4), NumberStyles.HexNumber);
                byte recType = byte.Parse(line.Substring(7, 2), NumberStyles.HexNumber);
                switch (recType)
                {
                    case dataRecord:
                        ProcessDataRecord(line, data, length, addr);
                        return true;
                    case eofRecord:
                        return false;
                    case segRecord :
                        ProcessSegmentRecord(line, data, length, addr);
                        return true;
                    default:
                        throw new FileFormatException();
                }
            }
            catch (FileFormatException)
            {
                throw;
            }
            catch (Exception ex)
            {
                throw new FileFormatException("The file has invalid format.", ex);
            }
        }

        private void ProcessDataRecord(String line, List<byte> data, byte length, ushort addr)
        {
            byte sum = (byte)(dataRecord + length + (byte)addr + (byte)(addr >> 8));
            if (line.Length != 2 * length + 11)
            {
                throw new FileFormatException();
            }
            int fullAddr = offset + addr;
            if (fullAddr > data.Count)
            {
                data.AddRange(GetDefaults(fullAddr - data.Count));
            }
            for (int i = 0; i < length; i++)
            {
                byte value = byte.Parse(line.Substring(2 * i + 9, 2), NumberStyles.HexNumber);
                if (fullAddr >= data.Count)
                    data.Add(value);
                else
                    data[fullAddr] = value;

                fullAddr++;
                sum = (byte)(sum + value);
            }
            sum += byte.Parse(line.Substring(line.Length - 2, 2), NumberStyles.HexNumber);
            if (sum != 0)
            {
                throw new FileFormatException();
            }
        }

        private void ProcessSegmentRecord(String line, List<byte> data, byte length, ushort addr)
        {
            if (length != 2 || addr != 0 || line.Length != 15)
            {
                throw new FileFormatException();
            }
            ushort seg = ushort.Parse(line.Substring(9, 2), NumberStyles.HexNumber);
            byte sum = (byte)(segRecord + length + (byte)seg + (byte)(seg >> 8));
            sum += byte.Parse(line.Substring(line.Length - 2, 2), NumberStyles.HexNumber);
            if (sum != 0)
            {
                throw new FileFormatException();
            }
            offset = seg << 4;
        }

        private IEnumerable<byte> GetDefaults(int count)
        {
            for (int i = 0; i < count; i++)
            {
                yield return DefaultValue;
            }
        }

        private int offset;

        private readonly StreamReader reader;

        private const byte dataRecord = 0x00;

        private const byte eofRecord = 0x01;

        private const byte segRecord = 0x02;

        #endregion

    }
}
